package com.liruo.treasureattic.service.url.service.impl;

import cn.hutool.core.util.ArrayUtil;
import com.liruo.idworker.distributed.DistributedIdWorker;
import com.liruo.treasureattic.common.httpmsg.exception.CommonResultException;
import com.liruo.treasureattic.common.httpmsg.exception.ValidatorException;
import com.liruo.treasureattic.common.httpmsg.result.Errno;
import com.liruo.treasureattic.common.model.dto.url.*;
import com.liruo.treasureattic.common.model.entity.TreasureUrl;
import com.liruo.treasureattic.common.model.entity.UrlFolder;
import com.liruo.treasureattic.common.model.enume.HtmlImportSelectionEnum;
import com.liruo.treasureattic.common.model.vo.url.UrlFolderVo;
import com.liruo.treasureattic.common.model.vo.url.UrlQueryVo;
import com.liruo.treasureattic.common.model.vo.url.UrlVo;
import com.liruo.treasureattic.common.util.NetUtils;
import com.liruo.treasureattic.common.util.ValidatorUtil;
import com.liruo.treasureattic.common.util.html.bookmark.ParseResult;
import com.liruo.treasureattic.common.util.html.bookmark.UrlImporter;
import com.liruo.treasureattic.common.util.html.bookmark.impl.CommonHtmlUrlImporter;
import com.liruo.treasureattic.common.util.html.bookmark.model.Folder;
import com.liruo.treasureattic.service.url.model.Accumulator;
import com.liruo.treasureattic.service.url.model.UrlConstant;
import com.liruo.treasureattic.service.url.service.UrlAndFolderService;
import com.liruo.treasureattic.starter.jwtlogin.holder.UserLoginStatusHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * @Author:liruo
 * @Date:2023-02-09-20:24:35
 * @Desc
 */
@Service
public class UrlAndFolderServiceImpl implements UrlAndFolderService {
    @Resource
    private TreasureUrlServiceImpl treasureUrlServiceImpl;
    @Resource
    private UrlFolderServiceImpl urlFolderServiceImpl;

    @Resource
    private DistributedIdWorker distributedIdWorker;
    @Override
    public UrlQueryVo queryOneLevel(UrlQueryOneLevelReq urlQueryOneLevelReq) {
        //检查参数
        ValidatorUtil.anyNull(urlQueryOneLevelReq);

        Long ownerId = urlQueryOneLevelReq.getOwnerId();
        Long folderId = urlQueryOneLevelReq.getFolderId();
        ValidatorUtil.requireGreat0(ownerId,folderId);

        //决定查询的url和folder的open属性
        boolean shouldOpen = shouldOpen(ownerId);

        //拿rootId找sub
        UrlFolder rootFolder;
        if(folderId == null){
            rootFolder = urlFolderServiceImpl.getDefaultFolder(ownerId);
        }else{
            rootFolder = urlFolderServiceImpl.getById(folderId);
        }
        if(rootFolder == null){
            throw new ValidatorException(Errno.NOT_EXISTS, "not exists the folder");
        }

        //因为是由root来查询的，因为root不用再重新发送了, 即获取子folder即可，但是如果是默认folder，需要加上
        long rootId = rootFolder.getId();
        List<UrlFolder> folders = urlFolderServiceImpl.getSubFolder(rootId, shouldOpen, ownerId);
        //获取rootFolder的子url， 如果是默认folder(没有url)则不用查
        List<TreasureUrl> urls;
        if(rootFolder.getFid() == UrlConstant.DEFAULT_FOLDER_FID){
            folders.add(rootFolder);
            urls = Collections.emptyList();
        }else{
            urls = treasureUrlServiceImpl.getUrlByFid(rootId, shouldOpen, ownerId);
        }

        //组装结果
        UrlQueryVo urlQueryVo = new UrlQueryVo();
        urlQueryVo.setUrlFolderVos(assembleUrlsQueryVo(folders, urls));
        return urlQueryVo;
    }
    /**
     * 查询时，判断是否查询open的url和folder
     * open == >   loginStatus!=null && loginStatus.userId = ownerId
     * noOpen == > loginStatus==null
     */
    private boolean shouldOpen(Long ownerId) {
        return UserLoginStatusHolder.isLogin() && ownerId.equals(UserLoginStatusHolder.getLoginStatus().getUserId());
    }
    public static List<UrlFolderVo> assembleUrlsQueryVo(List<UrlFolder> folders, List<TreasureUrl> urls) {
        List<UrlFolderVo> urlFolderVos = folders.stream()
                .map(urlFolder -> {
                    UrlFolderVo urlFolderVo = new UrlFolderVo();
                    BeanUtils.copyProperties(urlFolder, urlFolderVo);
                    return urlFolderVo;
                })
                .toList();
        List<UrlVo> urlVos = urls.stream()
                .map(url -> {
                    UrlVo urlVo = new UrlVo();
                    BeanUtils.copyProperties(urlVo, url);
                    return urlVo;
                })
                .toList();
        return  UrlFolderVo.build(urlVos, urlFolderVos);
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delFolder(long folderId) {
        //检查参数
        ValidatorUtil.requireGreat0(folderId);
        this.batchDelete(Collections.singleton(folderId));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void batchDelUrlFolder(List<Long> deleteFolderIds) {
        ValidatorUtil.requireGreat0(deleteFolderIds);
        batchDelete(deleteFolderIds);
    }

    /**
     * @param folderIds  batchDelete只读folderIds
     */
    @Transactional(rollbackFor = Exception.class)
    public void batchDelete(Collection<Long> folderIds) {
        Long userId = UserLoginStatusHolder.getLoginStatus().getUserId();
        //必须存在该folder
        if(urlFolderServiceImpl.isAllExists(folderIds, userId)){
            throw new CommonResultException(Errno.NOT_EXISTS, "delete folder must exists the folder");
        }

        List<Long> toDelUrlIds = new ArrayList<>();
        List<Long> toDelFolderIds = new ArrayList<>(folderIds);

        //一个folder的子folder的id是不可能重复的，因此不用set
        //删除folder
        List<Long> subFolderIds;
        List<Long> findDelFolderId = new ArrayList<>(folderIds);
        while(true){
            toDelUrlIds.addAll(treasureUrlServiceImpl.getUrlIdByFids(findDelFolderId, userId));
            subFolderIds = urlFolderServiceImpl.getSubFolderIdsByFids(findDelFolderId, userId);
            if(CollectionUtils.isEmpty(subFolderIds)){
                break;
            }
            findDelFolderId.clear();
            findDelFolderId.addAll(subFolderIds);
            toDelFolderIds.addAll(subFolderIds);
        }
        ValidatorUtil.dbOpt(treasureUrlServiceImpl.removeByIds(toDelUrlIds),
                "fail when  delete folders to delete urls");
        ValidatorUtil.dbOpt(urlFolderServiceImpl.removeByIds(toDelFolderIds),
                "fail when  delete folders to delete urls");
    }

    /**
     * 对 folder 和 url 的 batch 的 update 或者 delete
     * 不空时才删除或者修改
     */
    @Transactional(rollbackFor = Exception.class)
    public void batchUpdateOrDel(UrlBatchUpdateOrDelReq urlBatchUpdateOrDelReq) {
        ValidatorUtil.anyNull(urlBatchUpdateOrDelReq);
        //修改的
        if(!CollectionUtils.isEmpty(urlBatchUpdateOrDelReq.getChangedUrlList())){
            treasureUrlServiceImpl.batchUpdate(urlBatchUpdateOrDelReq.getChangedUrlList());
        }
        if(!CollectionUtils.isEmpty(urlBatchUpdateOrDelReq.getChangedFolderList())){
            urlFolderServiceImpl.batchUpdateFolder(urlBatchUpdateOrDelReq.getChangedFolderList());
        }

        //删除的
        if(!CollectionUtils.isEmpty(urlBatchUpdateOrDelReq.getDeleteUrlIds())){
            treasureUrlServiceImpl.batchDelUrl(urlBatchUpdateOrDelReq.getDeleteUrlIds());
        }
        if(!CollectionUtils.isEmpty(urlBatchUpdateOrDelReq.getDeleteFolderIds())){
            this.batchDelUrlFolder(urlBatchUpdateOrDelReq.getDeleteFolderIds());
        }
    }

    /**
     *  解析的rootFolder是一个总folder
     *        浏览器工具栏上的书签目录以及书签在一个叫toolbar的folder里,toolbar在rootFolder中
     *       其他书签就直接在rootFolder下
     *
     *     TO_DEFAULT_FOLDER(0,"把解析结果直接放到默认folder下")
     *          返回rootFolder
     *     TO_NEW_FOLDER(1,"把解析结果直接放到新建的folder下")
     *          返回新建的folder， 把rootFolder设置为urlFolderAddReq的name和open和fid
     *     以上2个使用collectUrlAndFolder进行对folder的id的抢占，因此最后使用update而不是save
     *
     *     SELF_ADJUST(2,"- 进行自我调整(把上传的与当前的进行重组)后再重新上传保存-结果返回后，前端应该再请求所有folder，这些folder会有点击就查询url的功能"),
     *          返回导入的folder和url视图，folder和url使用的都是临时的id(使用id生成器生成的)，在SELF_ADJUST_REUPLOAD时会设为null进行保存
     *     SELF_ADJUST_REUPLOAD(3, "自我调整的重新上传变化后的father关系的folder和url保存")
     *          删除了临时id，返回导入的数据 按数据库的id降序拿size个
     *         1. 需要把folder和url的假id删除
     *         2. 重新上传的包括本就有的和导入的，都存在被修改和被删除的情况，而且导入的被删除前端不会加入
     *
     *     3次保存都用saveOrUpdateAll，前2个是更新，第三个是保存
     */
    @Override
    public List<UrlFolderVo> importUrlByHtml(HtmlUrlImportReq htmlUrlImportReq) {
        //检查参数
        ValidatorUtil.anyNull(htmlUrlImportReq);

        if(htmlUrlImportReq.getHtmlImportSelectionEnum().equals(HtmlImportSelectionEnum.SELF_ADJUST_REUPLOAD)){
            HtmlParseReUploadReq htmlParseReUploadReq = htmlUrlImportReq.getHtmlParseReUploadReq();
            ValidatorUtil.anyNull(
                    "need HtmlParseReUploadReq SELF_ADJUST_REUPLOAD is " +
                    HtmlImportSelectionEnum.SELF_ADJUST_REUPLOAD.getDescription(),
                    htmlParseReUploadReq);
            return importUrlByHtmlSelfAdjustRealUpload(htmlParseReUploadReq);
        }

        //要求是html文件
        MultipartFile html = htmlUrlImportReq.getHtml();
        if(!MediaType.TEXT_HTML_VALUE.equalsIgnoreCase(html.getContentType())){
            throw new ValidatorException( Errno.PARAMS,
                    "require html file");
        }
        byte[] bytes;
        try {
            bytes = html.getBytes();
        } catch (IOException e) {
            throw new CommonResultException(Errno.SYSTEM,
                    "read upload file fail,err=" + e.getMessage());
        }
        if(ArrayUtil.isEmpty(bytes)){
            throw new ValidatorException(Errno.PARAMS, "require content");
        }

        //开始解析
        UrlImporter urlImporter = new CommonHtmlUrlImporter();
        ParseResult parseResult = urlImporter.parse(new String(bytes, StandardCharsets.UTF_8));

        ValidatorUtil.anyNull(parseResult.getRootFolder());

        return switch (htmlUrlImportReq.getHtmlImportSelectionEnum()){
            case TO_DEFAULT_FOLDER -> {
                importUrlByHtmlToDefaultFolder(parseResult);
                yield Collections.emptyList();
            }
            case TO_NEW_FOLDER -> {
                UrlFolderAddReq urlFolderAddReq = htmlUrlImportReq.getUrlFolderAddReq();
                ValidatorUtil.anyNull("want to build in a new folder but need folder name",
                        urlFolderAddReq);

                importUrlByHtmlToNewFolder(parseResult, urlFolderAddReq);
                yield Collections.emptyList();
            }
            case SELF_ADJUST -> importUrlByHtmlSelfAdjust(parseResult);
            default -> Collections.emptyList();
        };
    }
    /**
     * 把解析结果直接放到默认folder下
     * @return rootFolder
     */
    private List<UrlFolderVo> importUrlByHtmlToDefaultFolder(ParseResult parseResult) {
        long defaultFolderId = urlFolderServiceImpl.getDefaultFolderId(UserLoginStatusHolder.getLoginStatus().getUserId());

        //在数据库占用folder的id
        long occupyUrlFolderStartId = urlFolderServiceImpl.occupyUrlFolderId(parseResult.getFolderSize());
        Accumulator accumulator = new Accumulator(occupyUrlFolderStartId);

        //收集folder和url
        List<UrlFolder> urlFolders = new ArrayList<>();
        List<TreasureUrl> treasureUrls = new ArrayList<>();
        UrlFolder rootFolder = collectUrlAndFolder(accumulator, occupyUrlFolderStartId,
                parseResult.getRootFolder(),
                urlFolders, treasureUrls);

        //保存folder和url
        UrlAndFolderService urlAndFolderService = (UrlAndFolderService) AopContext.currentProxy();
        urlAndFolderService.saveOrUpdateAll(urlFolders, treasureUrls);

        UrlFolderVo urlFolderVo = new UrlFolderVo();
        BeanUtils.copyProperties(rootFolder, urlFolderVo);
        return Collections.singletonList(urlFolderVo);
    }

    /**
     * 把解析结果直接放到新建的folder下, 把rootFolder设置为urlFolderAddReq的name和open和fid
     * @return 新建的rootFolder
     */
    private List<UrlFolderVo> importUrlByHtmlToNewFolder(ParseResult parseResult, UrlFolderAddReq urlFolderAddReq) {
        //把root的name换为上传的新建的folder
        parseResult.getRootFolder().setName(urlFolderAddReq.getName());

        urlFolderServiceImpl.addFolderCheck(urlFolderAddReq);

        //在数据库占用folder的id
        long occupyUrlFolderStartId = urlFolderServiceImpl.occupyUrlFolderId(parseResult.getFolderSize());
        Accumulator accumulator = new Accumulator(occupyUrlFolderStartId);

        //收集folder和url
        List<UrlFolder> urlFolders = new ArrayList<>();
        List<TreasureUrl> treasureUrls = new ArrayList<>();
        UrlFolder rootFolder = collectUrlAndFolder(accumulator, urlFolderAddReq.getFid(),
                parseResult.getRootFolder(),
                urlFolders, treasureUrls);

        //把rootFolder设置为urlFolderAddReq的name和open和fid
        rootFolder.setName(urlFolderAddReq.getName());
        rootFolder.setOpen(urlFolderAddReq.getOpen());
        LocalDateTime now = LocalDateTime.now();
        rootFolder.setCtime(now);
        rootFolder.setUtime(now);
        rootFolder.setDeleted(false);

        //保存folder和url
        UrlAndFolderService urlAndFolderService = (UrlAndFolderService) AopContext.currentProxy();
        urlAndFolderService.saveOrUpdateAll(urlFolders, treasureUrls);


        UrlFolderVo urlFolderVo = new UrlFolderVo();
        BeanUtils.copyProperties(rootFolder, urlFolderVo);
        return Collections.singletonList(urlFolderVo);
    }

    /**
     * 为folder和url设置假的id---临时的
     * @return 导入的folder和url
     */
    private List<UrlFolderVo> importUrlByHtmlSelfAdjust(ParseResult parseResult) {
        List<UrlFolder> urlFolders = new ArrayList<>();
        List<TreasureUrl> treasureUrls = new ArrayList<>();
        Accumulator folderIdAccumulator = new Accumulator(distributedIdWorker.getId());
        collectUrlAndFolder(folderIdAccumulator, folderIdAccumulator.getAndIncr(),
                parseResult.getRootFolder(),
                urlFolders, treasureUrls);

        //为url设置假的id
        Accumulator urlIdAccumulator = new Accumulator(distributedIdWorker.getId());
        treasureUrls.forEach(url->url.setId(urlIdAccumulator.getAndIncr()));

        return  UrlAndFolderServiceImpl.assembleUrlsQueryVo(urlFolders, treasureUrls);
    }

    /**
     * 注意:
     *  1. 需要把folder和url的假id删除
     *  2. 重新上传的包括本就有的和导入的，都存在被修改和被删除的情况，而且导入的被删除前端不会加入
     * @return 导入的数据 按数据库的id降序拿size个
     */
    private List<UrlFolderVo> importUrlByHtmlSelfAdjustRealUpload(HtmlParseReUploadReq htmlParseReUploadReq) {
        List<UrlVo> parsedUrlVos = htmlParseReUploadReq.getParsedChangedUrlList();
        List<UrlFolderVo> parseUrlFolderVos = htmlParseReUploadReq.getParsedChangedFolderList();
        ValidatorUtil.allCollectionEmpty(
                "parsedUrlVos or parseUrlFolderVos is null or empty ",
                parsedUrlVos,
                parseUrlFolderVos
        );

        List<TreasureUrl> treasureUrls = Collections.emptyList();
        if(parsedUrlVos != null){
            treasureUrls = parsedUrlVos.stream()
                    .peek(url->ValidatorUtil.checkLen(url.getName(), UrlConstant.MIN_URL_NAME,
                            UrlConstant.MAX_URL_NAME))
                    .map(url -> {
                        TreasureUrl treasureUrl = new TreasureUrl();
                        BeanUtils.copyProperties(url, treasureUrl);
                        treasureUrl.setId(null);
                        return treasureUrl;
                    })
                    .toList();
        }
        List<UrlFolder> urlFolders = Collections.emptyList();
        if(parseUrlFolderVos != null){
            urlFolders = parseUrlFolderVos.stream()
                    .peek(folder->ValidatorUtil.checkLen(folder.getName(), UrlConstant.MIN_URL_FOLDER_NAME,
                            UrlConstant.MAX_URL_FOLDER_NAME))
                    .map(folder -> {
                        UrlFolder urlFolder = new UrlFolder();
                        BeanUtils.copyProperties(folder, urlFolder);
                        urlFolder.setId(null);
                        return urlFolder;
                    })
                    .toList();
        }

        UrlAndFolderService urlAndFolderService = (UrlAndFolderService)AopContext.currentProxy();
        //保存导入的
        urlAndFolderService.saveOrUpdateAll(urlFolders, treasureUrls);

        //处理已存在的修改和删除
        urlAndFolderService.batchUpdateOrDel(htmlParseReUploadReq.getUrlBatchUpdateOrDelReq());

        List<TreasureUrl> lastUrls = treasureUrlServiceImpl.getLast(urlFolders.size());
        List<UrlFolder> lastUrlFolders = urlFolderServiceImpl.getLast(treasureUrls.size());
        return assembleUrlsQueryVo(lastUrlFolders, lastUrls);
    }


    /**
     * 收集folder和url
     *  对folder: 设置id, uid, fid, name
     *  对url: 设置fid, uid, url, name, ico
     * @return rootFolder--也是ParseResult的rootFolder
     */
    private UrlFolder collectUrlAndFolder(Accumulator occupyUrlFolderStartId, long fatherId,
                                          Folder folder,
                                          List<UrlFolder> urlFolders, List<TreasureUrl> treasureUrls){
        Long userId = UserLoginStatusHolder.getLoginStatus().getUserId();
        UrlFolder rootFolder = new UrlFolder();
        rootFolder.setId(occupyUrlFolderStartId.getAndIncr());
        rootFolder.setUid(userId);
        rootFolder.setFid(fatherId);
        rootFolder.setName(folder.getName());

        urlFolders.add(rootFolder);

        if(!CollectionUtils.isEmpty(folder.getUrls())){
            treasureUrls.addAll(folder.getUrls().stream()
                    .map(url->{
                        TreasureUrl treasureUrl = new TreasureUrl();
                        treasureUrl.setFid(rootFolder.getId());
                        treasureUrl.setUid(userId);
                        treasureUrl.setUrl(url.getUrl());
                        treasureUrl.setName(url.getName());
                        treasureUrl.setIco(NetUtils.getIcoFormUrl(treasureUrl.getUrl(), UrlConstant.DEFAULT_ICON));
                        return treasureUrl;
                    })
                    .toList());
        }
        for (Folder sub : folder.getSubFolders()) {
            collectUrlAndFolder(occupyUrlFolderStartId, rootFolder.getId(),
                    sub,
                    urlFolders, treasureUrls);
        }
        return rootFolder;
    }
    @Transactional(rollbackFor = Exception.class)
    public void saveOrUpdateAll(List<UrlFolder> urlFolders, List<TreasureUrl> treasureUrls) {
        urlFolders.forEach(folder->
            ValidatorUtil.checkLen(folder.getName(), UrlConstant.MIN_URL_FOLDER_NAME,
                    UrlConstant.MAX_URL_FOLDER_NAME));
        treasureUrls.forEach(url-> {
            ValidatorUtil.checkLen(url.getName(), UrlConstant.MIN_URL_NAME,
                    UrlConstant.MAX_URL_NAME);
            ValidatorUtil.checkLen(url.getUrl(), UrlConstant.MIN_URL_NAME,
                    UrlConstant.MAX_URL_NAME);
        });

        if(!CollectionUtils.isEmpty(urlFolders)){
            ValidatorUtil.dbOpt(urlFolderServiceImpl.saveOrUpdateBatch(urlFolders),
                    "fail when batch save urlFolder");
        }
        if(!CollectionUtils.isEmpty(treasureUrls)){
            ValidatorUtil.dbOpt(treasureUrlServiceImpl.saveOrUpdateBatch(treasureUrls),
                    "fail when batch save urlFolder");
        }
    }
}
